/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

grammar smartidl;

options {
	output = AST;
	// backtrack = true;
	// memoize = true;
}

@header {
	package org.latestbit.smartobjects.smartidlc.parser;

	import java.util.ArrayList;
	import java.util.Collection;	
	import org.latestbit.smartobjects.smartidlc.model.*;
	import org.latestbit.smartobjects.smartidlc.util.*;		
}

@lexer::header {
	package org.latestbit.smartobjects.smartidlc.parser;
	
	import java.util.ArrayList;
	import java.util.Collection;	
	import org.latestbit.smartobjects.smartidlc.model.*;
	import org.latestbit.smartobjects.smartidlc.util.*;
}

@members { 
  protected void mismatch(IntStream input, int ttype, BitSet follow) 
    throws RecognitionException { 
    throw new MismatchedTokenException(ttype, input); 
  } 
  public void recoverFromMismatchedSet(
      IntStream input, RecognitionException e, BitSet follow) 
      throws RecognitionException { 
    throw e; 
  } 
} 
@rulecatch { 
  catch (RecognitionException e) { 
    throw e; 
  } 
} 




//****************************************************************************************************************************************************
// Parser
//****************************************************************************************************************************************************

// Intial point of compilation
startCompilation returns [IDLNamespace namespace]
@init {
	$namespace = new IDLNamespace();
}
	:	namespaceDeclaration [$namespace]
		importDeclarationList [$namespace]
		globalDeclarationList [$namespace]
		EOF		
	;

namespaceDeclaration [IDLNamespace namespace]
	:	'namespace' qualifiedName ';'
	{
		$namespace.setName ( $qualifiedName.text );
	}
	;
    
importDeclaration [IDLNamespace namespace]
	:	'import' qualifiedName ';'
	{
		$namespace.getImportedNamespacesLinks().add($qualifiedName.text);
	}
	;
	
importDeclarationList [IDLNamespace namespace]
	:	(importDeclaration[$namespace])*
	;		

globalDeclarationList [IDLNamespace namespace]
	:	(globalDeclaration[$namespace])*		
	;
	
globalDeclaration [IDLNamespace namespace]
	: 	( 
			typedefDeclaration
			{
				$namespace.getTypedefs().add ( $typedefDeclaration.idlTypeDef );
				$typedefDeclaration.idlTypeDef.setNamespace ( $namespace );
			} 
		| 	enumDeclaration
			{
				$namespace.getEnums().add ( $enumDeclaration.idlEnum );
				$enumDeclaration.idlEnum.setNamespace ( $namespace );
			}		 
		|	structureDeclaration
			{
				$namespace.getStructures().add ( $structureDeclaration.idlStructure );
				$structureDeclaration.idlStructure.setNamespace ( $namespace );
			}
		|	exceptionDeclaration
			{
				$namespace.getExceptions().add ( $exceptionDeclaration.idlException );
				$exceptionDeclaration.idlException.setNamespace ( $namespace );
			}			
		|	choiceDeclaration
			{
				$namespace.getChoices().add ( $choiceDeclaration.idlChoice );
				$choiceDeclaration.idlChoice.setNamespace ( $namespace );
			}		
		| 	interfaceDeclaration
			{
				$namespace.getInterfaces().add ( $interfaceDeclaration.idlInterface );
				$interfaceDeclaration.idlInterface.setNamespace ( $namespace );
			}		 
		) (';')?
	;
	
typedefDeclaration returns [IDLTypeDef idlTypeDef]
@init {
	$idlTypeDef = new IDLTypeDef();
}
	:	'typedef' type=datatypeDeclaration 'as' name=Identifier
		{
			$idlTypeDef.setName ( $name.text );
			$idlTypeDef.setRefDataType ( $type.idlDataType );	
		} 
	;
	
datatypeDeclaration returns [IDLDataType idlDataType]
@init {
	$idlDataType = new IDLDataType();
}
	:	('encrypted'{
			$idlDataType.setEncrypted(true);
		})?
		('required'{
			$idlDataType.setRequired(true);
		})?		
		( 
			primitiveType { $idlDataType.setTypeId(IDLDataType.DataTypeId.PRIMITIVE); $idlDataType.setPrimitiveType($primitiveType.primitiveType); } 
			| complexType { $idlDataType.setTypeId(IDLDataType.DataTypeId.COMPLEX); $idlDataType.setComplexType($complexType.complexType); }
			| qualifiedName { $idlDataType.setTypeId(IDLDataType.DataTypeId.QUALIFIED_NAME); $idlDataType.setName($qualifiedName.text);}
		)
	;
	
complexType returns [IDLComplexType complexType]
@init {
	$complexType = new IDLComplexType();
}
	:	arrayDeclaration [$complexType]
	|	listDeclaration [$complexType]
	|	mapDeclaration [$complexType]
	|   '(' ct = complexType ')' { $complexType.complexType = ct.complexType; }
	;
		
primitiveType returns [IDLPrimitiveType primitiveType]
@init {
	$primitiveType = new IDLPrimitiveType();
	boolean isUnsigned = false;
	boolean isUnicode = false;
}
	:	(	( ('unsigned' { isUnsigned = true; } )?
				(	'byte' { $primitiveType.setTypeId ( isUnsigned ? IDLPrimitiveType.TypeId.UNSIGNED_BYTE : IDLPrimitiveType.TypeId.BYTE ); }
					|	( 'int16') { $primitiveType.setTypeId ( isUnsigned ? IDLPrimitiveType.TypeId.UINT16 : IDLPrimitiveType.TypeId.INT16 ); }
					|	( 'int32') { $primitiveType.setTypeId ( isUnsigned ? IDLPrimitiveType.TypeId.UINT32 : IDLPrimitiveType.TypeId.INT32 ); }
					|	( 'int64') { $primitiveType.setTypeId ( isUnsigned ? IDLPrimitiveType.TypeId.UINT64 : IDLPrimitiveType.TypeId.INT64 ); }
				)					
			) (intRanges=integerTypeConstraint {$primitiveType.setRangeConstraints($intRanges.ranges); } )?			
			| 'integer' (integerRanges=integerTypeConstraint)?
				{
					if(integerRanges == null) {
						$primitiveType.setTypeId ( IDLPrimitiveType.TypeId.BIGINTEGER );
					}
					else {
						$primitiveType.setRangeConstraints($integerRanges.ranges);
						$primitiveType.setupIntTypeByConstraints();
					}
				}							
			|	(
					'float' { $primitiveType.setTypeId ( IDLPrimitiveType.TypeId.FLOAT ); }
					| 'double' { $primitiveType.setTypeId ( IDLPrimitiveType.TypeId.DOUBLE ); }
				) (floatRanges = floatTypeConstraint { $primitiveType.setRangeConstraints($floatRanges.ranges); })?
			|	(
					'number' { $primitiveType.setTypeId ( IDLPrimitiveType.TypeId.BIGDECIMAL ); }
					('max-precision' '=' maxprecision=integerLiteral { $primitiveType.setBigDecimalMaxPrecision( Integer.valueOf($maxprecision.text) ); } )? 
				) (floatRanges = floatTypeConstraint { $primitiveType.setRangeConstraints($floatRanges.ranges); })?			
			| ('unicode' { isUnicode = true; })? 'string' (strSizeConstraint = stringSizeConstraint)? (strMaskConstraint = stringMaskConstraint)? 
				{ 
					$primitiveType.setTypeId ( isUnicode ? IDLPrimitiveType.TypeId.WSTRING : IDLPrimitiveType.TypeId.STRING);
					if (strSizeConstraint !=null) {
						$primitiveType.setStringSizeConstraint( $strSizeConstraint.range );
					}
					if ( strMaskConstraint != null ) {
						$primitiveType.setStringMaskConstraints( $strMaskConstraint.masks );
					} 
				}
			| ('unicode' { isUnicode = true; })? 'char' { $primitiveType.setTypeId ( isUnicode ? IDLPrimitiveType.TypeId.WCHAR : IDLPrimitiveType.TypeId.CHAR); }		
			| 'boolean' { $primitiveType.setTypeId ( IDLPrimitiveType.TypeId.BOOLEAN ); }
			| 'datetime' { $primitiveType.setTypeId ( IDLPrimitiveType.TypeId.DATETIME ); }
		)
		
	;

integerTypeConstraint returns [ Collection < IDLPritiveTypeNumberRange<String> > ranges ]
@init {
	$ranges = new ArrayList< IDLPritiveTypeNumberRange<String> >() ;
	IDLPritiveTypeNumberRange<String> range = new IDLPritiveTypeNumberRange<String>();
}
	: '(' 
		min=integerLiteral { range.setMin("0"); range.setMax($min.text); } ('to' max=integerLiteral { range.setMin( range.getMax() ); range.setMax($max.text); })? {$ranges.add(range);} 
			(',' min=integerLiteral {  range = new IDLPritiveTypeNumberRange<String>(); range.setMin("0"); range.setMax($min.text); } ('to' max=integerLiteral { range.setMin( range.getMax() ); range.setMax($max.text); })? { $ranges.add(range);})*
	 ')'
	;
	
floatTypeConstraint returns [ Collection < IDLPritiveTypeNumberRange<String> > ranges ]
@init {
	$ranges = new ArrayList< IDLPritiveTypeNumberRange<String> >() ;
	IDLPritiveTypeNumberRange<String> range = new IDLPritiveTypeNumberRange<String>();
}
	: '(' 
		min=floatingPointLiteral { range.setMin("0"); range.setMax($min.text); } ('to' max=floatingPointLiteral { range.setMin( range.getMax() ); range.setMax($max.text); })? {$ranges.add(range);}
			(',' min=floatingPointLiteral {  range = new IDLPritiveTypeNumberRange<String>(); range.setMin("0"); range.setMax($min.text); } ('to' max=floatingPointLiteral { range.setMin( range.getMax() ); range.setMax($max.text); })? {$ranges.add(range);})*
	 ')'
	;

stringSizeConstraint returns [IDLPritiveTypeNumberRange<Integer> range]
@init {
	$range = new IDLPritiveTypeNumberRange<Integer>();
}
	:	'(' min=integerLiteral { $range.setMin(0); $range.setMax(Integer.valueOf($min.text)); } ('to' max=integerLiteral { $range.setMin($range.getMax()); $range.setMax(Integer.valueOf($max.text)); } )? ')' 
	;

stringMaskConstraint returns [Collection< String > masks]
@init {
	$masks = new ArrayList<String>();
}
	:
		'allowed' '(' mask=StringLiteral {$masks.add($mask.text);} (',' mask=StringLiteral {$masks.add($mask.text);})* ')'
	;

arrayDeclaration [IDLComplexType complexType]
	:	'array' '(' constraint=integerLiteral ')' 'of' itemDataType = datatypeDeclaration
		{
			$complexType.setComplexTypeId ( IDLComplexType.ComplexTypeId.ARRAY );
			$complexType.setComplexTypeSizeConstraint ( Integer.valueOf($constraint.text) );
			$complexType.setComplexItemDataType ( $itemDataType.idlDataType );
		}
	;

listDeclaration [IDLComplexType complexType]
	:	'list' ('(' constraint=integerLiteral ')')? 'of' itemDataType = datatypeDeclaration
		{
			$complexType.setComplexTypeId ( IDLComplexType.ComplexTypeId.LIST );
			if (constraint != null)
				$complexType.setComplexTypeSizeConstraint ( Integer.valueOf($constraint.text) );
			$complexType.setComplexItemDataType ( $itemDataType.idlDataType );
		}
	;

mapDeclaration [IDLComplexType complexType]
	:	'map' ('(' constraint=integerLiteral ')')? 'of' itemDataType = datatypeDeclaration 'by' keyType = primitiveType
		{
			$complexType.setComplexTypeId ( IDLComplexType.ComplexTypeId.MAP );
			if (constraint != null)
				$complexType.setComplexTypeSizeConstraint ( Integer.valueOf($constraint.text) );
			$complexType.setComplexItemDataType ( $itemDataType.idlDataType );
			$complexType.setComplexKeyDataType ( $keyType.primitiveType );
		}
	;

enumDeclaration returns [IDLEnum idlEnum]
@init {
	$idlEnum = new IDLEnum();
}
	:	'enum' name=Identifier { $idlEnum.setName($name.text); } '{' enumItem[$idlEnum] (',' enumItem[$idlEnum])* '}'
	;
	
enumItem [IDLEnum idlEnum]
	:	enumItemName=Identifier ('=' enumItemValueType=literal)?
	{
		CollectionPair <String, Object> pair = new CollectionPair<String, Object> ();
		pair.setFirst ( $enumItemName.text ); 
		if(enumItemValueType!=null) {
			pair.setSecond ( $enumItemValueType.text );
			$idlEnum.setItemDataTypeId ( $enumItemValueType.typeId );
		}
		else
			$idlEnum.setNoUserValues( true );
		$idlEnum.getItems().add ( pair );
	}
	;

structureDeclaration returns [IDLStructure idlStructure]
@init {
	$idlStructure = new IDLStructure();
}
	:	'structure' name=Identifier ('extends' parents=qualifiedNameList)? '{' (structureItem[$idlStructure])* '}'
	{
		$idlStructure.setName ( $name.text );
		if (parents != null)
			$idlStructure.setParentsLinks ( $parents.nameList ); 
	}
	;
	
structureItem [IDLStructure idlStructure]
	:	type=datatypeDeclaration name=Identifier ('=' value=literal)? ';'
	{
		IDLStructureItem item = new IDLStructureItem();
		item.setName ( $name.text );
		item.setType ( $type.idlDataType );
		if ( value != null) {
			item.setValue ( $value.text );
		}
		item.setDeclaredStructure ( idlStructure );
		$idlStructure.getItems().add ( item );		
	}
	;
	
exceptionDeclaration returns [IDLException idlException]
@init {
	$idlException = new IDLException();
}
	:	'exception' name=Identifier ('extends' parent=qualifiedName)? '{' (exceptionItem[$idlException])* '}'
	{
		$idlException.setName ( $name.text );
		if (parent != null)
			$idlException.setParentLink ( $parent.text ); 
	}
	;
	
exceptionItem [IDLException idlException]
	:	type=datatypeDeclaration name=Identifier ('=' value=literal)? ';'
	{
		IDLExceptionItem item = new IDLExceptionItem();
		item.setName ( $name.text );
		item.setType ( $type.idlDataType );
		if ( value != null) {
			item.setValue ( $value.text );
		}
		item.setDeclaredException ( idlException );
		$idlException.getItems().add ( item );
	}
	;

choiceDeclaration returns [IDLChoice idlChoice]
@init {
	$idlChoice = new IDLChoice();
}
	:	'choice' name=Identifier ('extends' parents=qualifiedNameList)? '{' (choiceItem[$idlChoice])* '}'
	{
		$idlChoice.setName ( $name.text );
		if (parents != null)
			$idlChoice.setParentsLinks ( $parents.nameList );
	}
	;
	
choiceItem [IDLChoice idlChoice]
	:	type=datatypeDeclaration name=Identifier ';'
	{
		IDLChoiceItem item = new IDLChoiceItem();
		item.setName ( $name.text );
		item.setType ( $type.idlDataType);
		$idlChoice.getItems().add ( item );
	}
	;
	
interfaceDeclaration returns [IDLInterface idlInterface]
@init {
	$idlInterface = new IDLInterface();
}
	:	'interface' name=Identifier ('extends' parents=qualifiedNameList)?  '{' (interfaceOperation[$idlInterface])* '}'
		{
			$idlInterface.setName ( $name.text );
			if (parents != null)
				$idlInterface.setParentsLinks ( $parents.nameList );	
		}
	;

interfaceOperation [IDLInterface idlInterface]
@init {
	IDLInterfaceOperation idlOperation = new IDLInterfaceOperation(); 
}
	:	interfaceOperationResult[idlOperation] name=Identifier '(' (interfaceOperationParam[idlOperation])? (',' interfaceOperationParam[idlOperation])* ')' (operationThrowsDeclaration[idlOperation])?  ';'
		{
			idlOperation.setName ( $name.text );
			idlOperation.setDeclaredInInterface ( idlInterface );
			$idlInterface.getOperations().add ( idlOperation );			
		}
	;
	
interfaceOperationResult [IDLInterfaceOperation idlOperation]
	:	(type=datatypeDeclaration (isSessionId='as' 'sessionid')?
			{
				$idlOperation.setResultType ($type.idlDataType);
				if (isSessionId!=null)
					$idlOperation.setResultSessionId ( true );				
			}
		)
		| 'void'
	;
	
interfaceOperationParam [IDLInterfaceOperation idlOperation]
	:	type=datatypeDeclaration name=Identifier ('=' value=literal)? (isSessionId='as' 'sessionid')?
		{
			IDLInterfaceOperationParam param = new IDLInterfaceOperationParam();
			param.setName ( $name.text );
			param.setType ( $type.idlDataType );
			if ( value != null )
				param.setDefaultValue ( $value.text );
			if (isSessionId!=null)
				param.setSessionId ( true );				
				
			$idlOperation.getParams().add ( param );
		}
	;
	
operationThrowsDeclaration [IDLInterfaceOperation idlOperation]
	:	'throws' exceptionList=qualifiedNameList
		{
			$idlOperation.setThrowsDeclLinks ( exceptionList.nameList );
		}
	;
	
qualifiedNameList returns [ Collection<String> nameList ]
@init {
	$nameList = new ArrayList < String > ();
}
    :   firstName=qualifiedName { $nameList.add($firstName.text); } (',' nextName=qualifiedName { $nameList.add($nextName.text); })*
    ;	

//****************************************************************************************************************************************************
// Lexer
//****************************************************************************************************************************************************
    
qualifiedName
    :   Identifier ('.' Identifier)*
    ;
    
literal returns [IDLPrimitiveType.TypeId typeId]
    :   intLit=integerLiteral { $typeId = $intLit.typeId; }
    |   fpLit=floatingPointLiteral { $typeId = $fpLit.typeId; }
    |   CharacterLiteral { $typeId = IDLPrimitiveType.TypeId.CHAR; }
    |   StringLiteral { $typeId = IDLPrimitiveType.TypeId.STRING; }
    |   booleanLiteral { $typeId = IDLPrimitiveType.TypeId.BOOLEAN; }
    |   'null' { $typeId = IDLPrimitiveType.TypeId.NULL; }
    ;

integerLiteral returns [IDLPrimitiveType.TypeId typeId]
    :  ( HexLiteral
    |   OctalLiteral
    |   DecimalLiteral )
    	sfx=IntegerTypeSuffix?
    	{
    		if ( sfx!=null ) {
    			$typeId = IDLPrimitiveType.TypeId.INT64;
    		}
    		else
    			$typeId = IDLPrimitiveType.TypeId.INT32;
    	}
    ;

booleanLiteral
    :   'true'
    |   'false'
    ;
    
HexLiteral : '0' ('x'|'X') HexDigit+  ;

DecimalLiteral : SignPrefix? ('0' | '1'..'9' '0'..'9'*) ;

OctalLiteral : '0' ('0'..'7')+ ;

fragment
HexDigit : ('0'..'9'|'a'..'f'|'A'..'F') ;

fragment
IntegerTypeSuffix : ('l'|'L') ;

fragment
SignPrefix : ('-') ;

floatingPointLiteral returns [IDLPrimitiveType.TypeId typeId] 
	: FloatingPointLiteral sfx=FloatTypeSuffix?
		{
			$typeId = IDLPrimitiveType.TypeId.FLOAT;
   			if(sfx!=null && $sfx.text.equalsIgnoreCase("d")) {
   				$typeId = IDLPrimitiveType.TypeId.DOUBLE;
   			} 
		}
	;

FloatingPointLiteral
    :  SignPrefix? ( 
    	('0'..'9')+ '.' ('0'..'9')* Exponent? 
    |   '.' ('0'..'9')+ Exponent?
    |   ('0'..'9')+ Exponent
    |   ('0'..'9')+ 
    )
    ;

fragment
Exponent : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;

fragment
FloatTypeSuffix : ('f'|'F'|'d'|'D') ;

CharacterLiteral
    :   '\'' ( EscapeSequence | ~('\''|'\\') ) '\''
    ;

StringLiteral
    :  '"' ( EscapeSequence | ~('\\'|'"') )* '"'
    ;

fragment
EscapeSequence
    :   '\\' ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\')
    |   UnicodeEscape
    |   OctalEscape
    ;

fragment
OctalEscape
    :   '\\' ('0'..'3') ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7')
    ;

fragment
UnicodeEscape
    :   '\\' 'u' HexDigit HexDigit HexDigit HexDigit
    ;
Identifier 
    :   Letter (Letter|DecimalLiteral)*
    ;

fragment
Letter
    :  '\u0024' |
       '\u0041'..'\u005a' |
       '\u005f' |
       '\u0061'..'\u007a' |
       '\u00c0'..'\u00d6' |
       '\u00d8'..'\u00f6' |
       '\u00f8'..'\u00ff' |
       '\u0100'..'\u1fff' |
       '\u3040'..'\u318f' |
       '\u3300'..'\u337f' |
       '\u3400'..'\u3d2d' |
       '\u4e00'..'\u9fff' |
       '\uf900'..'\ufaff'
    ;
WS  :  (' '|'\r'|'\t'|'\u000C'|'\n') {$channel=HIDDEN;}
    ;

COMMENT
    :   '/*' ( options {greedy=false;} : . )* '*/' {$channel=HIDDEN;}
    ;

LINE_COMMENT
    : '//' ~('\n'|'\r')* '\r'? '\n' {$channel=HIDDEN;}
    ;    
